package br.unicamp.ic.lis.dbvapi.service.impl;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.genericspatialdao.dao.DAO;
import org.genericspatialdao.dao.GenericSpatialDAO;

import br.unicamp.ic.lis.dbvapi.domain.AbstractPhysicalVersion;
import br.unicamp.ic.lis.dbvapi.domain.DBV;
import br.unicamp.ic.lis.dbvapi.domain.LogicalVersion;
import br.unicamp.ic.lis.dbvapi.domain.LogicalVersionPK;
import br.unicamp.ic.lis.dbvapi.domain.MultiversionObject;
import br.unicamp.ic.lis.dbvapi.domain.Scale;
import br.unicamp.ic.lis.dbvapi.service.LogicalVersionService;
import br.unicamp.ic.lis.dbvapi.utils.StampUtils;

public class LogicalVersionServiceImpl extends ServiceImpl<LogicalVersion>
		implements LogicalVersionService {

	private static Logger LOG = Logger
			.getLogger(LogicalVersionServiceImpl.class);

	public LogicalVersionServiceImpl() {
		super(LogicalVersion.class);
	}

	@Override
	public DAO<LogicalVersion> getDAO() {
		if (dao == null) {
			dao = new GenericSpatialDAO<LogicalVersion>(LogicalVersion.class);
		}
		return dao;
		// TODO
		// throw new
		// DBVException("Use LogicalVersionService API instead of getDAO()");
	}

	private DAO<LogicalVersion> getPrivateDAO() {
		if (dao == null) {
			dao = new GenericSpatialDAO<LogicalVersion>(LogicalVersion.class);
		}
		return dao;
	}

	/*
	 * @Override public List<LogicalVersion> getVersionedObjects(DBV dbv) {
	 * DBVService dbvService = new DBVServiceImpl(); List<LogicalVersion> result
	 * = new ArrayList<LogicalVersion>(); List<Long> visitedObjects = new
	 * ArrayList<Long>(); while (dbv != null) { List<LogicalVersion>
	 * partialResult = getDeclaredChanges(dbv); for (LogicalVersion vo :
	 * partialResult) { if (!visitedObjects.contains(vo.getLogicalVersionPK()
	 * .getObjectId())) {
	 * visitedObjects.add(vo.getLogicalVersionPK().getObjectId());
	 * result.add(vo); } } dbv = dbvService.getPrevious(dbv); } return result; }
	 */

	/**
	 * Algorithm: if there is an exactly lv in the DB do nothing return if there
	 * is an shared lv in the DB if sharedLv.pv == lv.pv do nothing else persist
	 * lv
	 */
	@Override
	public void synchronize(LogicalVersion lv) {
		LOG.info("Synchronizing logical version: " + lv);

		LogicalVersion lvDB = getPrivateDAO().find(lv.getLogicalVersionPK());

		// if it exists, makes an update
		if (lvDB != null) {
			LOG.info("Nothing to do. Logical version already exists");
			return;
		}

		// verify if exists a logical version parent (based on DBV parent)
		LogicalVersion lvSharedInDB = null;
		DBV parentDBV = lv.getDbv().getParent();
		while (parentDBV != null) {
			LogicalVersionPK lvParentPK = new LogicalVersionPK(
					lv.getMultiversionObject(), parentDBV);
			lvSharedInDB = getPrivateDAO().find(lvParentPK);
			parentDBV = lv.getDbv().getParent();
		}

		if (lvSharedInDB == null) {
			getPrivateDAO().persist(lv);
		} else {
			if (lv.getPhysicalVersion().equals(
					lvSharedInDB.getPhysicalVersion())) {
				// do nothing
				LOG.info("Nothing to do. Logical version shared");
			} else {
				LOG.info("Updating logical version to: " + lv);
				getPrivateDAO().persist(lv);
			}
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<LogicalVersion> getChanges(DBV dbv) {
		final String HQL_TEMPLATE = "FROM LogicalVersion lv WHERE dbv_dbvid = ?";
		return getDAO().executeHQL(HQL_TEMPLATE, dbv.getId());
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<LogicalVersion> getLogicalVersions(
			MultiversionObject multiVersionObject,
			AbstractPhysicalVersion<?> physicalVersion) {
		final String HQL_TEMPLATE = "FROM LogicalVersion lv WHERE multiversionObject = ? AND physicalVersion = ?";
		return getDAO().executeHQL(HQL_TEMPLATE, multiVersionObject,
				physicalVersion);
	}

	@Override
	public List<LogicalVersion> getLogicalVersions(String stamp, Scale scale) {
		DBV dbv = new DBVServiceImpl().getDBV(stamp, scale.getId());
		return getLogicalVersions(dbv);
	}

	@Override
	public List<LogicalVersion> getLogicalVersions(DBV dbv) {

		List<String> dbvStampsToReach = new ArrayList<String>();

		dbvStampsToReach.add(dbv.getStamp());
		String returnedStamp = StampUtils.getParentStamp(dbv.getStamp());
		while (returnedStamp != null) {
			dbvStampsToReach.add(returnedStamp);
			returnedStamp = StampUtils.getParentStamp(returnedStamp);
		}

		LOG.info("DBV stamps to reach: " + dbvStampsToReach);

		/**
		 * SELECT dbv_dbvid, object_oid, physicalversion_pvid FROM
		 * logical_version, dbv, dbv_dimension, dimension WHERE dimension.did =
		 * ? AND dbv.dbvid = dbv_dimension.dbvid AND dimension.did =
		 * dbv_dimension.did AND dbv_dbvid = dbv.dbvid
		 */

		StringBuilder hqlTemplate = new StringBuilder(
				"FROM LogicalVersion lv, DBV dbv WHERE lv.dbv = dbv.id AND dbv.scale.id = ");
		hqlTemplate.append(dbv.getScale().getId());
		hqlTemplate.append(" AND ");
		if (dbvStampsToReach.size() > 1) {
			hqlTemplate.append("(");
		}
		for (int i = 0; i < dbvStampsToReach.size(); i++) {
			hqlTemplate.append("dbv.stamp = '");
			hqlTemplate.append(dbvStampsToReach.get(i));
			hqlTemplate.append("'");
			if (!(i == dbvStampsToReach.size() - 1)) {
				hqlTemplate.append(" OR ");
			}
		}
		if (dbvStampsToReach.size() > 1) {
			hqlTemplate.append(")");
		}
		hqlTemplate.append(" order by dbv.stamp desc");
		String query = hqlTemplate.toString();

		@SuppressWarnings("unchecked")
		List<java.lang.Object[]> queryResult = getDAO().executeHQL(query);

		List<LogicalVersion> finalResult = new ArrayList<LogicalVersion>();
		List<Long> visitedObjects = new ArrayList<Long>();
		for (int i = 0; i < queryResult.size(); i++) {
			LogicalVersion logicalVersion = (LogicalVersion) queryResult.get(i)[0];
			if (!visitedObjects.contains(logicalVersion.getMultiversionObject()
					.getId())) {
				visitedObjects.add(logicalVersion.getMultiversionObject()
						.getId());
				// objects set to null will not be returned
				if (logicalVersion.getPhysicalVersion() != null) {
					finalResult.add(new LogicalVersion(logicalVersion
							.getMultiversionObject(), dbv, logicalVersion
							.getPhysicalVersion()));
				}
			}
		}

		LOG.info("Logical versions: " + finalResult);
		return finalResult;
	}

	@SuppressWarnings("unused")
	private LogicalVersion getLogicalVersionFromObject(
			List<LogicalVersion> list, MultiversionObject target) {
		for (int i = 0; i < list.size(); i++) {
			if (list.get(i).getMultiversionObject().equals(target)) {
				return list.get(i);
			}
		}
		return null;
	}

	public boolean isAncestor(DBV a, DBV b) {
		List<String> chainStampA = StampUtils.getStampChainAsList(a.getStamp());
		List<String> chainStampB = StampUtils.getStampChainAsList(b.getStamp());
		if (chainStampA.size() >= chainStampB.size()) {
			return false;
		}
		for (int i = 0; i < chainStampA.size(); i++) {
			if (!StringUtils.equalsIgnoreCase(chainStampA.get(i),
					chainStampB.get(i))) {
				return false;
			}
		}
		return true;
	}

	// Algorithm:
	// if there is an exactly lv in the DB
	// ----do nothing
	// ----return
	// if there is not an shared lv in the DB
	// ----persists lv
	// else if lv.pv is different of pv shared
	// ----persist lv
	@Override
	public void add(LogicalVersion lv) {
		LOG.info("Adding logical version: " + lv);

		LogicalVersion lvDB = getPrivateDAO().find(lv.getLogicalVersionPK());

		// if it exists, do nothing
		if (lvDB != null) {
			LOG.warn("Nothing to do. Logical version already exists");
			return;
		}

		// verify if exists a logical version parent (based on DBV parent)
		LogicalVersion lvSharedInDB = getLogicalVersionParent(lv);

		if (lvSharedInDB == null) {
			LOG.info("Shared logical version not found. Persisting a new one: "
					+ lv);
			getPrivateDAO().persist(lv);
		} else if (!lvSharedInDB.getPhysicalVersion().equals(
				lv.getPhysicalVersion())) {
			LOG.info("A different shared logical version found. Persisting a new one: "
					+ lv);
			getPrivateDAO().persist(lv);
		} else {
			LOG.info("Nothing to do. Logical version shared");
		}
	}

	// Algorithm:
	// if there is an exactly lv in the DB
	// ----update
	// ----return
	// if there is not an shared lv in the DB
	// ----do nothing
	// ----return
	// else if lv.pv is different of pv shared
	// ----persist lv
	@Override
	public void update(LogicalVersion lv) {
		LOG.info("Updating logical version: " + lv);

		LogicalVersion lvDB = getPrivateDAO().find(lv.getLogicalVersionPK());

		// if it exists, do nothing
		if (lvDB != null) {
			LOG.info("Logical version present in the DB. Updating");
			getPrivateDAO().merge(lv);
			return;
		}

		// verify if exists a logical version parent (based on DBV parent)
		LogicalVersion lvSharedInDB = getLogicalVersionParent(lv);

		if (lvSharedInDB == null) {
			LOG.info("Shared logical version not found. Nothing to do");
		} else if (!lvSharedInDB.getPhysicalVersion().equals(
				lv.getPhysicalVersion())) {
			LOG.info("A different shared logical version found. Persisting a new one to update the physical version: "
					+ lv);
			getPrivateDAO().persist(lv);
		} else {
			LOG.info("Nothing to do. Logical version shared");
		}

	}

	@Override
	public void remove(LogicalVersion lv) {
		LOG.info("Removing logical version: " + lv);
		getPrivateDAO().remove(lv);
	}

	/**
	 * verify if exists a logical version parent (based on DBV parent)
	 * 
	 * @param lv
	 * @return a logical version parent
	 */
	private LogicalVersion getLogicalVersionParent(LogicalVersion lv) {
		LOG.info("Verifying if exists a logical version parent (based on DBV parent) of logical version: "
				+ lv);
		LogicalVersion lvSharedInDB = null;
		DBV parentDBV = lv.getDbv().getParent();
		while (lvSharedInDB == null && parentDBV != null) {
			LogicalVersionPK lvParentPK = new LogicalVersionPK(
					lv.getMultiversionObject(), parentDBV);
			lvSharedInDB = getPrivateDAO().find(lvParentPK);
			parentDBV = parentDBV.getParent();
		}
		LOG.info("Result: " + lvSharedInDB);
		return lvSharedInDB;
	}
}
